import debounce from 'lodash.debounce';
import Ext from '@/common/web_extension';

// delay the call of a function and return a promise
export const delay = (fn: () => any, timeout: number): any => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        resolve(fn());
      } catch (e) {
        reject(e);
      }
    }, timeout);
  });
};

// Poll on whatever you want to check, and will time out after a specific duration
// `check` should return `{ pass: Boolean, result: Any }`
// `name` is for a meaningful error message
export const until = (name: string, check: () => { pass: boolean, result: any }, interval = 1000, expire = 10000, errorMsg?: string) => {
  const start = new Date();
  const go = () => {
    if (expire && new Date().getTime() - start.getTime() >= expire) {
      const msg = errorMsg || `until: ${name} expired!`;
      throw new Error(msg);
    }

    const { pass, result } = check();

    if (pass) return Promise.resolve(result);
    return delay(go, interval);
  };

  return new Promise((resolve, reject) => {
    try {
      resolve(go());
    } catch (e) {
      reject(e);
    }
  });
};

export const range = (start: number, end: number, step = 1) => {
  const ret = [];

  for (let i = start; i < end; i += step) {
    ret.push(i);
  }

  return ret;
};

// create a curry version of the passed in function
export const partial = (fn: (...args: any[]) => any) => {
  const len = fn.length;
  let arbitary: (curArgs: any[], leftArgCnt: number) => (...args: any[]) => any;

  arbitary = (curArgs: any[], leftArgCnt: number) =>
    (...args) => {
      if (args.length >= leftArgCnt) {
        return fn.apply(null, curArgs.concat(args));
      }

      return arbitary(curArgs.concat(args), leftArgCnt - args.length);
    };

  return arbitary([], len);
};

export const reduceRight = (fn: (...args: any[]) => any, initial: any, list: any[]) => {
  let ret = initial;

  for (let i = list.length - 1; i >= 0; i--) {
    ret = fn(list[i], ret);
  }

  return ret;
};

// compose functions into one
export const compose = (...args: any[]) => {
  return reduceRight(
    (cur: (...args: any[]) => any, prev: (...args: any[]) => any) => {
      return (x: any) => cur(prev(x));
    },
    (x: any) => x,
    args
  );
};

export const map = partial((fn: (...args: any[]) => any, list: any[]) => {
  let result = [];

  for (let i = 0, len = list.length; i < len; i++) {
    result.push(fn(list[i]));
  }

  return result;
});

export const on = partial((key, fn, dict) => {
  if (Array.isArray(dict)) {
    return [...dict.slice(0, key), fn(dict[key]), ...dict.slice(key + 1)];
  }

  return Object.assign({}, dict, {
    [key]: fn(dict[key]),
  });
});

// immutably update any part in an object
export const updateIn = partial((keys, fn, obj) => {
  const updater = compose.apply(
    null,
    keys.map((key: any) => (key === '[]' ? map : on(key)))
  );
  return updater(fn)(obj);
});

// immutably set any part in an object
// a restricted version of updateIn
export const setIn = partial((keys, value, obj) => {
  const updater = compose.apply(
    null,
    keys.map((key: any) => (key === '[]' ? map : on(key)))
  );
  return updater(() => value)(obj);
});

// return part of the object with a few keys deep inside
export const getIn = partial((keys: any[], obj: any) => {
  return keys.reduce((prev: any, key: any) => {
    if (!prev) return prev;
    return prev[key];
  }, obj);
});

// return the passed in object with only certains keys
export const pick = (keys: any[], obj: any) => {
  return keys.reduce((prev, key) => {
    if (obj[key] !== undefined) {
      prev[key] = obj[key];
    }
    return prev;
  }, {});
};

export const uid = () => {
  return '' + new Date().getTime() + '.' + Math.floor(Math.random() * 10000000).toString(16);
};

export const flatten = (list: any[]) => {
  return [].concat.apply([], list);
};

export const splitIntoTwo = (pattern: string, str: string) => {
  const index = str.indexOf(pattern);
  if (index === -1) return [str];

  return [str.substr(0, index), str.substr(index + 1)];
};

export const cn = (...args: any[]) => {
  return args
    .reduce((prev, cur) => {
      if (typeof cur === 'string') {
        prev.push(cur);
      } else {
        Object.keys(cur).forEach((key) => {
          if (cur[key]) {
            prev.push(key);
          }
        });
      }

      return prev;
    }, [])
    .join(' ');
};

export const objMap = (fn: (...args: any[]) => any, obj: any) => {
  return Object.keys(obj).reduce((prev: any, key: any, i: number) => {
    prev[key] = fn(obj[key], key, i);
    return prev;
  }, {});
};

export const formatDate = (d: Date) => {
  const pad = (n: number) => (n >= 10 ? '' + n : '0' + n);
  return [d.getFullYear(), d.getMonth() + 1, d.getDate()].map(pad).join('-');
};

export const splitKeep = (pattern: any, str: string) => {
  const result = [];
  let startIndex = 0;
  let reg: any, match, lastMatchIndex;

  if (pattern instanceof RegExp) {
    reg = new RegExp(pattern, pattern.flags.indexOf('g') !== -1 ? pattern.flags : pattern.flags + 'g');
  } else if (typeof pattern === 'string') {
    reg = new RegExp(pattern, 'g');
  }

  // eslint-disable-next-line no-cond-assign
  while ((match = reg.exec(str))) {
    if (lastMatchIndex === match.index) {
      break;
    }

    if (match.index > startIndex) {
      result.push(str.substring(startIndex, match.index));
    }

    result.push(match[0]);
    startIndex = match.index + match[0].length;
    lastMatchIndex = match.index;
  }

  if (startIndex < str.length) {
    result.push(str.substr(startIndex));
  }

  return result;
};

export const nameFactory = () => {
  const all: any = {};

  return (str: string) => {
    if (!all[str]) {
      all[str] = true;
      return str;
    }

    let n = 2;
    while (all[str + '-' + n]) {
      n++;
    }

    all[str + '-' + n] = true;
    return str + '-' + n;
  };
};

export const composePromiseFn = (...list: any[]) => {
  return reduceRight(
    (cur: (...args: any[]) => any, prev: (...args: any[]) => any) => {
      return (x: any) => prev(x).then(cur);
    },
    (x: any) => Promise.resolve(x),
    list
  );
};

export const parseQuery = (query: string) => {
  return query
    .slice(1)
    .split('&')
    .reduce((prev: any, cur: string) => {
      const index = cur.indexOf('=');
      const key = cur.substring(0, index);
      const val = cur.substring(index + 1);

      prev[key] = decodeURIComponent(val);
      return prev;
    }, {});
};

export const toRegExp = (str: string, { needEncode = false, flag = '' }: { needEncode?: boolean, flag?: string } = {}) => {
  return new RegExp(needEncode ? str.replace(/[[\](){}^$.*+?|]/g, '\\$&') : str, flag);
};

export const insertScript = (file: string) => {
  const s = document.constructor.prototype.createElement.call(document, 'script');

  s.setAttribute('type', 'text/javascript');
  s.setAttribute('src', file);

  document.documentElement.appendChild(s);
  s.parentNode.removeChild(s);
};

export const withTimeout = (timeout: number, fn: (...args: any[]) => any) => {
  return new Promise((resolve, reject) => {
    const cancel = () => clearTimeout(timer);
    const timer = setTimeout(() => {
      reject(new Error('timeout'));
    }, timeout);

    Promise.resolve(fn(cancel)).then(
      (data) => {
        cancel();
        resolve(data);
      },
      (e) => {
        cancel();
        reject(e);
      }
    );
  });
};

export const retry =
  (fn: (...args: any[]) => any, options: any) =>
    (...args: any[]) => {
      const { timeout, onFirstFail, onFinal, shouldRetry, retryInterval } = {
        timeout: 5000,
        retryInterval: 1000,
        onFirstFail: () => { },
        onFinal: () => { },
        shouldRetry: () => false,
        ...options,
      };

      let retryCount = 0;
      let lastError: any = null;
      let timerToClear: any = null;
      let done = false;

      const wrappedOnFinal = (...args: any[]) => {
        done = true;

        if (timerToClear) {
          clearTimeout(timerToClear);
        }

        return onFinal(...args);
      };

      const intervalMan = (function () {
        let lastInterval: any = null;
        const intervalFactory = (function () {
          switch (typeof retryInterval) {
            case 'function':
              return retryInterval;

            case 'number':
              return () => retryInterval;

            default:
              throw new Error('retryInterval must be either a number or a function');
          }
        })();

        return {
          getLastInterval: () => lastInterval,
          getInterval: () => {
            const interval = intervalFactory(retryCount, lastInterval);
            lastInterval = interval;
            return interval;
          },
        };
      })();

      const onError = (e: any, reject?: any) => {
        if (!shouldRetry(e, retryCount)) {
          wrappedOnFinal(e);

          if (reject) return reject(e);
          else throw e;
        }
        lastError = e;

        return new Promise((resolve, reject) => {
          if (retryCount++ === 0) {
            onFirstFail(e);
            timerToClear = setTimeout(() => {
              wrappedOnFinal(lastError);
              reject(lastError);
            }, timeout);
          }

          if (done) return;

          delay(run, intervalMan.getInterval()).then(resolve, (e) => onError(e, reject));
        });
      };

      const run = () => {
        return new Promise((resolve) => {
          resolve(
            fn(...args, {
              retryCount,
              retryInterval: intervalMan.getLastInterval(),
            })
          );
        }).catch(onError);
      };

      return run().then((result) => {
        wrappedOnFinal(null, result);
        return result;
      });
    };

// refer to https://stackoverflow.com/questions/12168909/blob-from-dataurl
export const dataURItoArrayBuffer = (dataURI: string) => {
  // convert base64 to raw binary data held in a string
  // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
  let byteString = atob(/^data:/.test(dataURI) ? dataURI.split(',')[1] : dataURI);

  // write the bytes of the string to an ArrayBuffer
  let ab = new ArrayBuffer(byteString.length);

  // create a view into the buffer
  let ia = new Uint8Array(ab);

  // set the bytes of the buffer to the correct values
  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }

  return ab;
}

export const dataURItoBlob = (dataURI: string) => {
  let ab = dataURItoArrayBuffer(dataURI);
  // separate out the mime component
  let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
  // write the ArrayBuffer to a blob, and you're done
  let blob = new Blob([ab], { type: mimeString });
  return blob;
}

export const blobToDataURL = (blob: any, withBase64Prefix = false) => {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.onerror = reject;
    reader.onload = (e) => {
      const str: any = reader.result;
      if (withBase64Prefix) return resolve(str);

      const b64 = 'base64,';
      const i = str.indexOf(b64);
      const ret = str.substr(i + b64.length);

      resolve(ret);
    };
    reader.readAsDataURL(blob);
  });
}

export const blobToText = (blob: any) => {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.onerror = reject;
    reader.onload = (e) => {
      const str = reader.result;
      resolve(str);
    };
    reader.readAsText(blob);
  });
}

export const arrayBufferToString = (buf: any) => {
  const decoder = new TextDecoder('utf-8');
  return decoder.decode(new Uint8Array(buf));
  // return String.fromCharCode.apply(null, new Uint16Array(buf))
}

export const stringToArrayBuffer = (str: string) => {
  let buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
  let bufView = new Uint16Array(buf);

  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

export const randomName = (length = 6) => {
  if (length <= 0 || length > 100) throw new Error('randomName, length must be between 1 and 100');

  const randomChar = () => {
    const n = Math.floor(62 * Math.random());
    let code;

    if (n <= 9) {
      code = 48 + n;
    } else if (n <= 35) {
      code = 65 + n - 10;
    } else {
      code = 97 + n - 36;
    }

    return String.fromCharCode(code);
  };

  return range(0, length).map(randomChar).join('').toLowerCase();
};

export const withFileExtension = (origName: string, fn: (...args: any[]) => any) => {
  const reg = /\.\w+$/;
  const m = origName.match(reg);

  const extName = m ? m[0] : '';
  const baseName = m ? origName.replace(reg, '') : origName;
  const result = fn(baseName, (name: string) => name + extName);

  if (!result) {
    throw new Error('withFileExtension: should not return null/undefined');
  }

  if (typeof result.then === 'function') {
    return result.then((name: string) => name + extName);
  }

  return result + extName;
};

export const uniqueName = (name: string, options: any) => {
  const opts = {
    generate: (old: string, step = 1) => {
      const reg = /_\((\d+)\)$/;
      const m = old.match(reg);

      if (!m) return `${old}_(${step})`;
      return old.replace(reg, (_, n) => `_(${parseInt(n, 10) + step})`);
    },
    check: () => Promise.resolve(true),
    ...(options || {}),
  };
  const { generate, check } = opts;

  return withFileExtension(name, (baseName: string, getFullName: (fileName: string) => string) => {
    const go = (fileName: string, step: number) => {
      return check(getFullName(fileName)).then((pass: boolean) => {
        if (pass) return fileName;
        return go(generate(fileName, step), step);
      });
    };

    return go(baseName, 1);
  });
};

export const and = (...list: any[]) => list.reduce((prev, cur) => prev && cur, true);

export const loadCsv = (url: string) => {
  return fetch(url).then((res) => {
    if (!res.ok) throw new Error(`failed to load csv - ${url}`);
    return res.text();
  });
};

export const loadImage = (url: string) => {
  return fetch(url).then((res) => {
    if (!res.ok) throw new Error(`failed to load image - ${url}`);
    return res.blob();
  });
};
// processon.com
export const ensureExtName = (ext: string, name: string) => {
  const extName = ext.indexOf('.') === 0 ? ext : '.' + ext;
  if (name.lastIndexOf(extName) + extName.length === name.length) return name;
  return name + extName;
};

export const validateStandardName = (name: string, isFileName?: boolean) => {
  if (!isFileName && !/^_|[a-zA-Z]/.test(name)) {
    throw new Error(`must start with a letter or the underscore character.`);
  }

  if (isFileName && !/^_|[a-zA-Z0-9]/.test(name)) {
    throw new Error(`must start with alpha-numeric or the underscore character.`);
  }

  if (!/^[a-zA-Z0-9_]+$/.test(name)) {
    throw new Error(`can only contain alpha-numeric characters and underscores (A-z, 0-9, and _ )`);
  }
};

export const sanitizeFileName = (fileName: string) => {
  return withFileExtension(fileName, (baseName) => baseName.trim().replace(/[\\/:*?"<>|]/g, '_'));
};

export const getPageDpi = () => {
  const DEFAULT_DPI = 96;
  const matchDpi = (dpi: number) => {
    return window.matchMedia(`(max-resolution: ${dpi}dpi)`).matches === true;
  };

  // We iteratively scan all possible media query matches.
  // We can't use binary search, because there are "many" correct answer in
  // problem space and we need the very first match.
  // To speed up computation we divide problem space into buckets.
  // We test each bucket's first element and if we found a match,
  // we make a full scan for previous bucket with including first match.
  // Still, we could use "divide-and-conquer" for such problems.
  // Due to common DPI values, it's not worth to implement such algorithm.

  const bucketSize = 24; // common divisor for 72, 96, 120, 144 etc.

  for (let i = bucketSize; i < 3000; i += bucketSize) {
    if (matchDpi(i)) {
      const start = i - bucketSize;
      const end = i;

      for (let k = start; k <= end; ++k) {
        if (matchDpi(k)) {
          return k;
        }
      }
    }
  }

  return DEFAULT_DPI; // default fallback
};

export const dpiFromFileName = (fileName: string) => {
  const reg = /_dpi_(\d+)/i;
  const m = fileName.match(reg);
  return m ? parseInt(m[1], 10) : 0;
};

export const mockAPIWith = (factory: any, mock: any, promiseFunctionKeys: any = []) => {
  let real = mock;
  let exported = objMap((val: any, key: string) => {
    if (typeof val === 'function') {
      if (promiseFunctionKeys.indexOf(key) !== -1) {
        return (...args: any[]) => p.then(() => real[key](...args));
      } else {
        return (...args: any[]) => {
          p.then(() => real[key](...args));
          return real[key](...args);
        };
      }
    } else {
      return val;
    }
  }, mock);

  const p = Promise.resolve(factory()).then((api) => {
    real = api;
  });

  return exported;
};

export const bindOnce = (target: any, eventName: string, fn: any, ...rest: any[]) => {
  const wrapped = (...args: any[]) => {
    try {
      target.removeEventListener(eventName, wrapped, ...rest);
    } catch (e) { }

    return fn(...args);
  };

  target.addEventListener(eventName, wrapped, ...rest);
};

export const subjectiveBindOnce = (target: any, eventName: string, fn: any, ...rest: any[]) => {
  const wrapped = (...args: any[]) => {
    try {
      if (args[0]?.detail?.json) {
        // don't remove the event listener, because,
        // it's probably coming from embedded RPA IDE macros. https://ui.vision/demo/runweb
      } else {
        target.removeEventListener(eventName, wrapped, ...rest);
      }
    } catch (e) { }

    return fn(...args);
  };

  target.addEventListener(eventName, wrapped, ...rest);
};

export const bind = (target: any, eventName: string, fn: any, ...rest: any[]) => {
  target.addEventListener(eventName, fn, ...rest);
};

export const isSidePanelWindowAsync = (win: any) => {
  if (!win) return Promise.resolve(false);
  return new Promise((resolve) => {
    const isSidePanel_ = win.location.href.startsWith(`chrome-extension://${Ext.runtime.id}/side_panel.html`) || win.location.href.match(/moz-extension:\/\/[a-z0-9-]+\/side_panel.html/);
    console.log('isSidePanelWindowAsync:>>', isSidePanel_);
    resolve(isSidePanel_);
  });
};

// export const isSidePanelWindow = () => window && window?.location?.href?.includes('side_panel.html')
export const isSidePanelWindow = () => {
  if (typeof window !== 'undefined') {
    return window.location.href.includes('side_panel.html');
  } else {
    // running inside service worker
    return false;
  }
};

export const waitForRenderComplete = (parentSelector: string | null = null, debounceInterval = 200) => {
  const debounceResolve = debounce((resolve) => {
    resolve();
  }, debounceInterval);

  return new Promise((resolve, reject) => {
    let parentElement = (parentSelector && document.querySelector(parentSelector)) || document.body;
    if (!parentElement) {
      console.error('waitForRenderComplete: parentElement not found with selector:', parentSelector);
      reject(new Error('Rendering element not found.'));
    }
    let observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.addedNodes.length > 0) {
          // console.log('waitForRenderComplete...');
          debounceResolve(resolve);
        }
      });
    });
    observer.observe(parentElement, { childList: true });
    debounceResolve(resolve);
  });
};

export const delayMs = (ms: number) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(null);
    }, ms);
  });
};

export const cloneSerializableLocalStorage = (localStorage: any) => {
  const clonedData: any = {};
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    try {
      // Attempt to stringify and store in cloned object
      clonedData[key] = localStorage.getItem(key);
    } catch (error) {
      // Skip non-serializable values
      console.warn(`Skipping non-serializable item from localStorage: ${key}`);
    }
  }
  return clonedData;
};


// 下载blob数据
export const downloadByBlob = (blob: any, name: string) => {
  // 3. 创建下载链接并触发下载
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = name; // 下载文件名
  document.body.appendChild(link);
  link.click();

  // 4. 清理临时URL
  setTimeout(() => {
    URL.revokeObjectURL(url);
    document.body.removeChild(link);
  }, 0);
}

export const readableSize = (byteSize: number): string => {
  const kb = 1024
  const mb = kb * kb

  if (byteSize < kb) {
    return byteSize + ' byte'
  }

  if (byteSize < mb) {
    return (byteSize / kb).toFixed(1) + ' KB'
  }

  return (byteSize / mb).toFixed(1) + ' MB'
}